home *** CD-ROM | disk | FTP | other *** search
/ Personal Computer World 2005 October / PCWOCT05.iso / Software / FromTheMag / XAMPP 1.4.14 / xampp-win32-1.4.14-installer.exe / xampp / php / pear / HTML / Template / Sigma.php < prev   
PHP Script  |  2004-10-01  |  64KB  |  1,731 lines

  1. <?php
  2. //
  3. // +----------------------------------------------------------------------+
  4. // | PHP Version 4                                                        |
  5. // +----------------------------------------------------------------------+
  6. // | Copyright (c) 1997-2003 The PHP Group                                |
  7. // +----------------------------------------------------------------------+
  8. // | This source file is subject to version 2.02 of the PHP license,      |
  9. // | that is bundled with this package in the file LICENSE, and is        |
  10. // | available at through the world-wide-web at                           |
  11. // | http://www.php.net/license/2_02.txt.                                 |
  12. // | If you did not receive a copy of the PHP license and are unable to   |
  13. // | obtain it through the world-wide-web, please send a note to          |
  14. // | license@php.net so we can mail you a copy immediately.               |
  15. // +----------------------------------------------------------------------+
  16. // | Authors: Ulf Wendel <ulf.wendel@phpdoc.de>                           |
  17. // |          Alexey Borzov <avb@php.net>                                 |
  18. // +----------------------------------------------------------------------+
  19. //
  20. // $Id: Sigma.php,v 1.11 2004/05/31 14:09:42 avb Exp $
  21. //
  22.  
  23. require_once 'PEAR.php';
  24.  
  25. define('SIGMA_OK',                         1);
  26. define('SIGMA_ERROR',                     -1);
  27. define('SIGMA_TPL_NOT_FOUND',             -2);
  28. define('SIGMA_BLOCK_NOT_FOUND',           -3);
  29. define('SIGMA_BLOCK_DUPLICATE',           -4);
  30. define('SIGMA_CACHE_ERROR',               -5);
  31. define('SIGMA_UNKNOWN_OPTION',            -6);
  32. define('SIGMA_PLACEHOLDER_NOT_FOUND',     -10);
  33. define('SIGMA_PLACEHOLDER_DUPLICATE',     -11);
  34. define('SIGMA_BLOCK_EXISTS',              -12);
  35. define('SIGMA_INVALID_CALLBACK',          -13);
  36.  
  37. /**
  38. * HTML_Template_Sigma: implementation of Integrated Templates API with 
  39. * template 'compilation' added.
  40. *
  41. * The main new feature in Sigma is the template 'compilation'. Consider the
  42. * following: when loading a template file the engine has to parse it using
  43. * regular expressions to find all the blocks and variable placeholders. This
  44. * is a very "expensive" operation and is definitely an overkill to do on 
  45. * every page request: templates seldom change on production websites. This is
  46. * where the cache kicks in: it saves an internal representation of the 
  47. * template structure into a file and this file gets loaded instead of the 
  48. * source one on subsequent requests (unless the source changes, of course).
  49. * While HTML_Template_Sigma inherits PHPLib Template's template syntax, it has
  50. * an API which is easier to understand. When using HTML_Template_PHPLIB, you
  51. * have to explicitly name a source and a target the block gets parsed into.
  52. * This gives maximum flexibility but requires full knowledge of template 
  53. * structure from the programmer.
  54. * Integrated Template on the other hands manages block nesting and parsing 
  55. * itself. The engine knows that inner1 is a child of block2, there's
  56. * no need to tell it about this:
  57. *
  58. * + __global__ (hidden and automatically added)
  59. *     + block1
  60. *     + block2
  61. *         + inner1
  62. *         + inner2
  63. *
  64. * To add content to block1 you simply type:
  65. * <code>$tpl->setCurrentBlock("block1");</code>
  66. * and repeat this as often as needed:
  67. * <code>
  68. *   $tpl->setVariable(...);
  69. *   $tpl->parseCurrentBlock();
  70. * </code>
  71. *
  72. * To add content to block2 you would type something like:
  73. * <code>
  74. * $tpl->setCurrentBlock("inner1");
  75. * $tpl->setVariable(...);
  76. * $tpl->parseCurrentBlock();
  77. *
  78. * $tpl->setVariable(...);
  79. * $tpl->parseCurrentBlock();
  80. *
  81. * $tpl->parse("block2");
  82. * </code>
  83. *
  84. * This will result in one repetition of block2 which contains two repetitions
  85. * of inner1. inner2 will be removed if $removeEmptyBlock is set to true (which 
  86. * is the default).
  87. *
  88. * Usage:
  89. * <code>
  90. * $tpl = new HTML_Template_Sigma( [string filerootdir], [string cacherootdir] );
  91. *
  92. * // load a template or set it with setTemplate()
  93. * $tpl->loadTemplatefile( string filename [, boolean removeUnknownVariables, boolean removeEmptyBlocks] )
  94. *
  95. * // set "global" Variables meaning variables not beeing within a (inner) block
  96. * $tpl->setVariable( string variablename, mixed value );
  97. *
  98. * // like with the HTML_Template_PHPLIB there's a second way to use setVariable()
  99. * $tpl->setVariable( array ( string varname => mixed value ) );
  100. *
  101. * // Let's use any block, even a deeply nested one
  102. * $tpl->setCurrentBlock( string blockname );
  103. *
  104. * // repeat this as often as you need it.
  105. * $tpl->setVariable( array ( string varname => mixed value ) );
  106. * $tpl->parseCurrentBlock();
  107. *
  108. * // get the parsed template or print it: $tpl->show()
  109. * $html = $tpl->get();
  110. * </code>
  111. *
  112. * @author   Ulf Wendel <ulf.wendel@phpdoc.de>
  113. * @author   Alexey Borzov <avb@php.net>
  114. * @version  $Revision: 1.11 $
  115. * @access   public
  116. * @package  HTML_Template_Sigma
  117. */
  118. class HTML_Template_Sigma extends PEAR
  119. {
  120.    /**
  121.     * First character of a variable placeholder ( _{_VARIABLE} ).
  122.     * @var      string
  123.     * @access   public
  124.     * @see      $closingDelimiter, $blocknameRegExp, $variablenameRegExp
  125.     */
  126.     var $openingDelimiter = '{';
  127.  
  128.    /**
  129.     * Last character of a variable placeholder ( {VARIABLE_}_ )
  130.     * @var      string
  131.     * @access   public
  132.     * @see      $openingDelimiter, $blocknameRegExp, $variablenameRegExp
  133.     */
  134.     var $closingDelimiter = '}';
  135.  
  136.    /**
  137.     * RegExp for matching the block names in the template.
  138.     * Per default "sm" is used as the regexp modifier, "i" is missing.
  139.     * That means a case sensitive search is done.
  140.     * @var      string
  141.     * @access   public
  142.     * @see      $variablenameRegExp, $openingDelimiter, $closingDelimiter
  143.     */
  144.     var $blocknameRegExp = '[0-9A-Za-z_-]+';
  145.  
  146.    /**
  147.     * RegExp matching a variable placeholder in the template.
  148.     * Per default "sm" is used as the regexp modifier, "i" is missing.
  149.     * That means a case sensitive search is done.
  150.     * @var      string    
  151.     * @access   public
  152.     * @see      $blocknameRegExp, $openingDelimiter, $closingDelimiter
  153.     */
  154.     var $variablenameRegExp = '[0-9A-Za-z_-]+';
  155.  
  156.    /**
  157.     * RegExp used to find variable placeholder, filled by the constructor
  158.     * @var      string    Looks somewhat like @(delimiter varname delimiter)@
  159.     * @see      HTML_Template_Sigma()
  160.     */
  161.     var $variablesRegExp = '';
  162.  
  163.    /**
  164.     * RegExp used to strip unused variable placeholders
  165.     * @see      $variablesRegExp, HTML_Template_Sigma()
  166.     */
  167.     var $removeVariablesRegExp = '';
  168.  
  169.    /**
  170.     * RegExp used to find blocks and their content, filled by the constructor
  171.     * @var      string
  172.     * @see      HTML_Template_Sigma()
  173.     */
  174.     var $blockRegExp = '';
  175.  
  176.    /**
  177.     * Controls the handling of unknown variables, default is remove
  178.     * @var      boolean
  179.     * @access   public
  180.     */
  181.     var $removeUnknownVariables = true;
  182.  
  183.    /**
  184.     * Controls the handling of empty blocks, default is remove
  185.     * @var      boolean
  186.     * @access   public
  187.     */
  188.     var $removeEmptyBlocks = true;
  189.  
  190.    /**
  191.     * Name of the current block
  192.     * @var      string
  193.     */
  194.     var $currentBlock = '__global__';
  195.  
  196.    /**
  197.     * Template blocks and their content
  198.     * @var      array
  199.     * @see      _buildBlocks()
  200.     * @access   private
  201.     */
  202.     var $_blocks = array();
  203.  
  204.    /**
  205.     * Content of parsed blocks
  206.     * @var      array
  207.     * @see      get(), parse()
  208.     * @access   private
  209.     */
  210.     var $_parsedBlocks = array();
  211.  
  212.    /**
  213.     * Variable names that appear in the block
  214.     * @var      array
  215.     * @see      _buildBlockVariables()
  216.     * @access   private
  217.     */
  218.     var $_blockVariables = array();
  219.  
  220.    /**
  221.     * Inner blocks inside the block
  222.     * @var      array
  223.     * @see      _buildBlocks()
  224.     * @access   private
  225.     */
  226.     var $_children = array();
  227.  
  228.    /**
  229.     * List of blocks to preserve even if they are "empty"
  230.     * @var      array
  231.     * @see      touchBlock(), $removeEmptyBlocks
  232.     * @access   private
  233.     */
  234.     var $_touchedBlocks = array();
  235.  
  236.    /**
  237.     * List of blocks which should not be shown even if not "empty"
  238.     * @var      array
  239.     * @see      hideBlock(), $removeEmptyBlocks
  240.     * @access   private
  241.     */
  242.     var $_hiddenBlocks = array();
  243.  
  244.    /**
  245.     * Variables for substitution.
  246.     *
  247.     * Variables are kept in this array before the replacements are done.
  248.     * This allows automatic removal of empty blocks.
  249.     * 
  250.     * @var      array
  251.     * @see      setVariable()
  252.     * @access   private
  253.     */
  254.     var $_variables = array();
  255.  
  256.    /**
  257.     * Global variables for substitution
  258.     * 
  259.     * These are substituted into all blocks, are not cleared on
  260.     * block parsing and do not trigger "non-empty" logic. I.e. if 
  261.     * only global variables are substituted into the block, it is
  262.     * still considered "empty".
  263.     *
  264.     * @var      array
  265.     * @see      setVariable(), setGlobalVariable()
  266.     * @access   private
  267.     */
  268.     var $_globalVariables = array();
  269.  
  270.    /**
  271.     * Root directory for "source" templates
  272.     * @var    string
  273.     * @see    HTML_Template_Sigma(), setRoot()
  274.     */
  275.     var $fileRoot = '';
  276.  
  277.    /**
  278.     * Directory to store the "prepared" templates in
  279.     * @var      string
  280.     * @see      HTML_Template_Sigma(), setCacheRoot()
  281.     * @access   private
  282.     */
  283.     var $_cacheRoot = null;
  284.  
  285.    /**
  286.     * Flag indicating that the global block was parsed
  287.     * @var    boolean
  288.     */
  289.     var $flagGlobalParsed = false;
  290.  
  291.    /**
  292.     * Options to control some finer aspects of Sigma's work.
  293.     * 
  294.     * $_options['preserve_data'] If false, then substitute variables and remove empty 
  295.     * placeholders in data passed through setVariable (see also bugs #20199, #21951)
  296.     * $_options['trim_on_save'] Whether to trim extra whitespace from template on cache save.
  297.     * Generally safe to have this on, unless you have <pre></pre> in templates or want to 
  298.     * preserve HTML indentantion
  299.     */
  300.     var $_options = array(
  301.         'preserve_data' => false,
  302.         'trim_on_save'  => true
  303.     );
  304.  
  305.    /**
  306.     * Function name prefix used when searching for function calls in the template
  307.     * @var    string
  308.     */
  309.     var $functionPrefix = 'func_';
  310.  
  311.    /**
  312.     * Function name RegExp
  313.     * @var    string
  314.     */
  315.     var $functionnameRegExp = '[_a-zA-Z]+[A-Za-z_0-9]*';
  316.  
  317.    /**
  318.     * RegExp used to grep function calls in the template (set by the constructor)
  319.     * @var    string
  320.     * @see    _buildFunctionlist(), HTML_Template_Sigma()
  321.     */
  322.     var $functionRegExp = '';
  323.  
  324.    /**
  325.     * List of functions found in the template.
  326.     * @var    array
  327.     * @access private
  328.     */
  329.     var $_functions = array();
  330.  
  331.    /**
  332.     * List of callback functions specified by the user
  333.     * @var    array
  334.     * @access private
  335.     */
  336.     var $_callback = array();
  337.  
  338.    /**
  339.     * RegExp used to find file inclusion calls in the template (should have 'e' modifier)
  340.     * @var  string
  341.     */
  342.     var $includeRegExp = '#<!--\s+INCLUDE\s+(\S+)\s+-->#ime';
  343.  
  344.    /**
  345.     * Files queued for inclusion
  346.     * @var    array
  347.     * @access private
  348.     */
  349.     var $_triggers = array();
  350.  
  351.  
  352.    /**
  353.     * Constructor: builds some complex regular expressions and optionally 
  354.     * sets the root directories.
  355.     *
  356.     * Make sure that you call this constructor if you derive your template
  357.     * class from this one.
  358.     *
  359.     * @param string  root directory for templates
  360.     * @param string  directory to cache "prepared" templates in
  361.     * @see   setRoot(), setCacheRoot()
  362.     */
  363.     function HTML_Template_Sigma($root = '', $cacheRoot = '')
  364.     {
  365.         // the class is inherited from PEAR to be able to use $this->setErrorHandling()
  366.         $this->PEAR();
  367.         $this->variablesRegExp       = '@' . $this->openingDelimiter . '(' . $this->variablenameRegExp . ')' .
  368.                                        '(:(' . $this->functionnameRegExp . '))?' . $this->closingDelimiter . '@sm';
  369.         $this->removeVariablesRegExp = '@'.$this->openingDelimiter.'\s*('.$this->variablenameRegExp.')\s*'.$this->closingDelimiter.'@sm';
  370.         $this->blockRegExp           = '@<!--\s+BEGIN\s+('.$this->blocknameRegExp.')\s+-->(.*)<!--\s+END\s+\1\s+-->@sm';
  371.         $this->functionRegExp        = '@' . $this->functionPrefix . '(' . $this->functionnameRegExp . ')\s*\(@sm';
  372.         $this->setRoot($root);
  373.         $this->setCacheRoot($cacheRoot);
  374.  
  375.         $this->setCallbackFunction('h', 'htmlspecialchars');
  376.         $this->setCallbackFunction('u', 'urlencode');
  377.         $this->setCallbackFunction('j', array(&$this, '_jsEscape'));
  378.     }
  379.  
  380.  
  381.    /**
  382.     * Sets the file root for templates. The file root gets prefixed to all 
  383.     * filenames passed to the object.
  384.     * 
  385.     * @param    string  directory name
  386.     * @see      HTML_Template_Sigma()
  387.     * @access   public
  388.     */
  389.     function setRoot($root)
  390.     {
  391.         if (('' != $root) && ('/' != substr($root, -1))) {
  392.             $root .= '/';
  393.         }
  394.         $this->fileRoot = $root;
  395.     }
  396.  
  397.  
  398.    /**
  399.     * Sets the directory to cache "prepared" templates in, the directory should be writable for PHP.
  400.     * 
  401.     * The "prepared" template contains an internal representation of template 
  402.     * structure: essentially a serialized array of $_blocks, $_blockVariables, 
  403.     * $_children and $_functions, may also contain $_triggers. This allows 
  404.     * to bypass expensive calls to _buildBlockVariables() and especially 
  405.     * _buildBlocks() when reading the "prepared" template instead of 
  406.     * the "source" one.
  407.     * 
  408.     * The files in this cache do not have any TTL and are regenerated when the
  409.     * source templates change.
  410.     * 
  411.     * @param    string  directory name
  412.     * @see      HTML_Template_Sigma(), _getCached(), _writeCache()
  413.     * @access   public
  414.     */
  415.     function setCacheRoot($root)
  416.     {
  417.         if (empty($root)) {
  418.             return true;
  419.         } elseif (('' != $root) && ('/' != substr($root, -1))) {
  420.             $root .= '/';
  421.         }
  422.         $this->_cacheRoot = $root;
  423.     }
  424.  
  425.  
  426.    /**
  427.     * Sets the option for the template class
  428.     * 
  429.     * @access public
  430.     * @param  string  option name
  431.     * @param  mixed   option value
  432.     * @return mixed   SIGMA_OK on success, error object on failure
  433.     */
  434.     function setOption($option, $value)
  435.     {
  436.         if (isset($this->_options[$option])) {
  437.             $this->_options[$option] = $value;
  438.             return SIGMA_OK;
  439.         }
  440.         return $this->raiseError($this->errorMessage(SIGMA_UNKNOWN_OPTION, $option), SIGMA_UNKNOWN_OPTION);
  441.     }
  442.  
  443.  
  444.    /**
  445.     * Returns a textual error message for an error code
  446.     *  
  447.     * @access public
  448.     * @param  integer  error code
  449.     * @param  string   additional data to insert into message
  450.     * @return string   error message
  451.     */
  452.     function errorMessage($code, $data = null)
  453.     {
  454.         static $errorMessages;
  455.         if (!isset($errorMessages)) {
  456.             $errorMessages = array(
  457.                 SIGMA_ERROR                 => 'unknown error',
  458.                 SIGMA_OK                    => '',
  459.                 SIGMA_TPL_NOT_FOUND         => 'Cannot read the template file \'%s\'',
  460.                 SIGMA_BLOCK_NOT_FOUND       => 'Cannot find block \'%s\'',
  461.                 SIGMA_BLOCK_DUPLICATE       => 'The name of a block must be unique within a template. Block \'%s\' found twice.',
  462.                 SIGMA_CACHE_ERROR           => 'Cannot save template file \'%s\'',
  463.                 SIGMA_UNKNOWN_OPTION        => 'Unknown option \'%s\'',
  464.                 SIGMA_PLACEHOLDER_NOT_FOUND => 'Variable placeholder \'%s\' not found',
  465.                 SIGMA_PLACEHOLDER_DUPLICATE => 'Placeholder \'%s\' should be unique, found in multiple blocks',
  466.                 SIGMA_BLOCK_EXISTS          => 'Block \'%s\' already exists',
  467.                 SIGMA_INVALID_CALLBACK      => 'Callback does not exist'
  468.             );
  469.         }
  470.  
  471.         if (PEAR::isError($code)) {
  472.             $code = $code->getCode();
  473.         }
  474.         if (!isset($errorMessages[$code])) {
  475.             return $errorMessages[SIGMA_ERROR];
  476.         } else {
  477.             return (null === $data)? $errorMessages[$code]: sprintf($errorMessages[$code], $data);
  478.         }
  479.     }
  480.  
  481.  
  482.    /**
  483.     * Prints a block with all replacements done.
  484.     * 
  485.     * @access  public
  486.     * @param   string  block name
  487.     * @see     get()
  488.     */
  489.     function show($block = '__global__')
  490.     {
  491.         print $this->get($block);
  492.     }
  493.  
  494.  
  495.    /**
  496.     * Returns a block with all replacements done.
  497.     * 
  498.     * @param    string     block name
  499.     * @param    bool       whether to clear parsed block contents
  500.     * @return   string     block with all replacements done
  501.     * @throws   PEAR_Error
  502.     * @access   public
  503.     * @see      show()
  504.     */
  505.     function get($block = '__global__', $clear = false)
  506.     {
  507.         if (!isset($this->_blocks[$block])) {
  508.             return $this->raiseError($this->errorMessage(SIGMA_BLOCK_NOT_FOUND, $block), SIGMA_BLOCK_NOT_FOUND);
  509.         }
  510.         if ('__global__' == $block && !$this->flagGlobalParsed) {
  511.             $this->parse('__global__');
  512.         }
  513.         // return the parsed block, removing the unknown placeholders if needed
  514.         if (!isset($this->_parsedBlocks[$block])) {
  515.             return '';
  516.  
  517.         } else {
  518.             $ret = $this->_parsedBlocks[$block];
  519.             if ($clear) {
  520.                 unset($this->_parsedBlocks[$block]);
  521.             }
  522.             if ($this->removeUnknownVariables) {
  523.                 $ret = preg_replace($this->removeVariablesRegExp, '', $ret);
  524.             }
  525.             if ($this->_options['preserve_data']) {
  526.                 $ret = str_replace($this->openingDelimiter . '%preserved%' . $this->closingDelimiter, $this->openingDelimiter, $ret);
  527.             }
  528.             return $ret;
  529.         }
  530.     }
  531.  
  532.  
  533.    /**
  534.     * Parses the given block.
  535.     *    
  536.     * @param    string    block name
  537.     * @param    boolean   true if the function is called recursively (do not set this to true yourself!)
  538.     * @param    boolean   true if parsing a "hidden" block (do not set this to true yourself!)
  539.     * @access   public
  540.     * @see      parseCurrentBlock()
  541.     * @throws   PEAR_Error
  542.     */
  543.     function parse($block = '__global__', $flagRecursion = false, $fakeParse = false)
  544.     {
  545.         static $vars;
  546.  
  547.         if (!isset($this->_blocks[$block])) {
  548.             return $this->raiseError($this->errorMessage(SIGMA_BLOCK_NOT_FOUND, $block), SIGMA_BLOCK_NOT_FOUND);
  549.         }
  550.         if ('__global__' == $block) {
  551.             $this->flagGlobalParsed = true;
  552.         }
  553.         if (!isset($this->_parsedBlocks[$block])) {
  554.             $this->_parsedBlocks[$block] = '';
  555.         }
  556.         $outer = $this->_blocks[$block];
  557.  
  558.         if (!$flagRecursion) {
  559.             $vars = array();
  560.         }
  561.         // block is not empty if its local var is substituted
  562.         $empty = true;
  563.         foreach ($this->_blockVariables[$block] as $allowedvar => $v) {
  564.             if (isset($this->_variables[$allowedvar])) {
  565.                 $vars[$this->openingDelimiter . $allowedvar . $this->closingDelimiter] = $this->_variables[$allowedvar];
  566.                 $empty = false;
  567.                 // vital for checking "empty/nonempty" status
  568.                 unset($this->_variables[$allowedvar]);
  569.             }
  570.         }
  571.  
  572.         // processing of the inner blocks
  573.         if (isset($this->_children[$block])) {
  574.             foreach ($this->_children[$block] as $innerblock => $v) {
  575.                 $placeholder = $this->openingDelimiter.'__'.$innerblock.'__'.$this->closingDelimiter;
  576.  
  577.                 if (isset($this->_hiddenBlocks[$innerblock])) {
  578.                     // don't bother actually parsing this inner block; but we _have_
  579.                     // to go through its local vars to prevent problems on next iteration
  580.                     $this->parse($innerblock, true, true);
  581.                     unset($this->_hiddenBlocks[$innerblock]);
  582.                     $outer = str_replace($placeholder, '', $outer);
  583.  
  584.                 } else {
  585.                     $this->parse($innerblock, true, $fakeParse);
  586.                     // block is not empty if its inner block is not empty
  587.                     if ('' != $this->_parsedBlocks[$innerblock]) {
  588.                         $empty = false;
  589.                     }
  590.  
  591.                     $outer = str_replace($placeholder, $this->_parsedBlocks[$innerblock], $outer);
  592.                     $this->_parsedBlocks[$innerblock] = '';
  593.                 }
  594.             }
  595.         }
  596.  
  597.         // add "global" variables to the static array
  598.         foreach ($this->_globalVariables as $allowedvar => $value) {
  599.             if (isset($this->_blockVariables[$block][$allowedvar])) {
  600.                 $vars[$this->openingDelimiter . $allowedvar . $this->closingDelimiter] = $value;
  601.             }
  602.         }
  603.         // if we are inside a hidden block, don't bother
  604.         if (!$fakeParse) {
  605.             if (0 != count($vars) && (!$flagRecursion || !empty($this->_functions[$block]))) {
  606.                 $varKeys     = array_keys($vars);
  607.                 $varValues   = $this->_options['preserve_data']? array_map(array(&$this, '_preserveOpeningDelimiter'), array_values($vars)): array_values($vars);
  608.             }
  609.  
  610.             // check whether the block is considered "empty" and append parsed content if not
  611.             if (!$empty || ('__global__' == $block) || !$this->removeEmptyBlocks || isset($this->_touchedBlocks[$block])) {
  612.                 // perform callbacks
  613.                 if (!empty($this->_functions[$block])) {
  614.                     foreach ($this->_functions[$block] as $id => $data) {
  615.                         $placeholder = $this->openingDelimiter . '__function_' . $id . '__' . $this->closingDelimiter;
  616.                         // do not waste time calling function more than once
  617.                         if (!isset($vars[$placeholder])) {
  618.                             $args         = array();
  619.                             $preserveArgs = isset($this->_callback[$data['name']]['preserveArgs']) && $this->_callback[$data['name']]['preserveArgs'];
  620.                             foreach ($data['args'] as $arg) {
  621.                                 $args[] = (empty($varKeys) || $preserveArgs)? $arg: str_replace($varKeys, $varValues, $arg);
  622.                             }
  623.                             if (isset($this->_callback[$data['name']]['data'])) {
  624.                                 $res = call_user_func_array($this->_callback[$data['name']]['data'], $args);
  625.                             } else {
  626.                                 $res = isset($args[0])? $args[0]: '';
  627.                             }
  628.                             $outer = str_replace($placeholder, $res, $outer);
  629.                             // save the result to variable cache, it can be requested somewhere else
  630.                             $vars[$placeholder] = $res;
  631.                         }
  632.                     }
  633.                 }
  634.                 // substitute variables only on non-recursive call, thus all
  635.                 // variables from all inner blocks get substituted
  636.                 if (!$flagRecursion && !empty($varKeys)) {
  637.                     $outer = str_replace($varKeys, $varValues, $outer);
  638.                 }
  639.  
  640.                 $this->_parsedBlocks[$block] .= $outer;
  641.                 if (isset($this->_touchedBlocks[$block])) {
  642.                     unset($this->_touchedBlocks[$block]);
  643.                 }
  644.             }
  645.         }
  646.         return $empty;
  647.     }
  648.  
  649.  
  650.    /**
  651.     * Sets a variable value.
  652.     * 
  653.     * The function can be used either like setVariable("varname", "value")
  654.     * or with one array $variables["varname"] = "value" given setVariable($variables)
  655.     * 
  656.     * @access public
  657.     * @param  mixed     variable name or array ('varname'=>'value')
  658.     * @param  string    variable value if $variable is not an array
  659.     */
  660.     function setVariable($variable, $value = '')
  661.     {
  662.         if (is_array($variable)) {
  663.             $this->_variables = array_merge($this->_variables, $variable);
  664.         } else {
  665.             $this->_variables[$variable] = $value;
  666.         }
  667.     }
  668.  
  669.  
  670.    /**
  671.     * Sets a global variable value.
  672.     * 
  673.     * @access public
  674.     * @param  mixed     variable name or array ('varname'=>'value')
  675.     * @param  string    variable value if $variable is not an array
  676.     * @see    setVariable()
  677.     */
  678.     function setGlobalVariable($variable, $value = '')
  679.     {
  680.         if (is_array($variable)) {
  681.             $this->_globalVariables = array_merge($this->_globalVariables, $variable);
  682.         } else {
  683.             $this->_globalVariables[$variable] = $value;
  684.         }
  685.     }
  686.  
  687.  
  688.    /**
  689.     * Sets the name of the current block: the block where variables are added
  690.     *
  691.     * @param    string      block name
  692.     * @return   mixed       SIGMA_OK on success, error object on failure
  693.     * @throws   PEAR_Error
  694.     * @access   public
  695.     */
  696.     function setCurrentBlock($block = '__global__')
  697.     {
  698.         if (!isset($this->_blocks[$block])) {
  699.             return $this->raiseError($this->errorMessage(SIGMA_BLOCK_NOT_FOUND, $block), SIGMA_BLOCK_NOT_FOUND);
  700.         }
  701.         $this->currentBlock = $block;
  702.         return SIGMA_OK;
  703.     }
  704.  
  705.  
  706.    /**
  707.     * Parses the current block
  708.     * 
  709.     * @see      parse(), setCurrentBlock()
  710.     * @access   public
  711.     */
  712.     function parseCurrentBlock()
  713.     {
  714.         return $this->parse($this->currentBlock);
  715.     }
  716.  
  717.  
  718.    /**
  719.     * Returns the current block name
  720.     *
  721.     * @return string    block name
  722.     * @access public
  723.     */
  724.     function getCurrentBlock()
  725.     {
  726.         return $this->currentBlock;
  727.     }
  728.  
  729.  
  730.    /**
  731.     * Preserves the block even if empty blocks should be removed.
  732.     *
  733.     * Sometimes you have blocks that should be preserved although they are 
  734.     * empty (no placeholder replaced). Think of a shopping basket. If it's 
  735.     * empty you have to show a message to the user. If it's filled you have
  736.     * to show the contents of the shopping basket. Now where to place the 
  737.     * message that the basket is empty? It's not a good idea to place it 
  738.     * in you application as customers tend to like unecessary minor text
  739.     * changes. Having another template file for an empty basket means that 
  740.     * one fine day the filled and empty basket templates will have different
  741.     * layouts. 
  742.     * 
  743.     * So blocks that do not contain any placeholders but only messages like 
  744.     * "Your shopping basked is empty" are intoduced. Now if there is no 
  745.     * replacement done in such a block the block will be recognized as "empty"
  746.     * and by default ($removeEmptyBlocks = true) be stripped off. To avoid this
  747.     * you can call touchBlock()
  748.     *
  749.     * @param    string      block name
  750.     * @return   mixed       SIGMA_OK on success, error object on failure
  751.     * @throws   PEAR_Error    
  752.     * @access   public
  753.     * @see      $removeEmptyBlocks, $_touchedBlocks
  754.     */
  755.     function touchBlock($block)
  756.     {
  757.         if (!isset($this->_blocks[$block])) {
  758.             return $this->raiseError($this->errorMessage(SIGMA_BLOCK_NOT_FOUND, $block), SIGMA_BLOCK_NOT_FOUND);
  759.         }
  760.         if (isset($this->_hiddenBlocks[$block])) {
  761.             unset($this->_hiddenBlocks[$block]);
  762.         }
  763.         $this->_touchedBlocks[$block] = true;
  764.         return SIGMA_OK;
  765.     }
  766.  
  767.  
  768.    /**
  769.     * Hides the block even if it is not "empty".
  770.     * 
  771.     * Is somewhat an opposite to touchBlock().
  772.     * 
  773.     * Consider a block (a 'edit' link for example) that should be visible to 
  774.     * registered/"special" users only, but its visibility is triggered by 
  775.     * some little 'id' field passed in a large array into setVariable(). You 
  776.     * can either carefully juggle your variables to prevent the block from 
  777.     * appearing (a fragile solution) or simply call hideBlock()
  778.     *
  779.     * @param    string      block name
  780.     * @return   mixed       SIGMA_OK on success, error object on failure
  781.     * @throws   PEAR_Error    
  782.     * @access   public
  783.     */
  784.     function hideBlock($block)
  785.     {
  786.         if (!isset($this->_blocks[$block])) {
  787.             return $this->raiseError($this->errorMessage(SIGMA_BLOCK_NOT_FOUND, $block), SIGMA_BLOCK_NOT_FOUND);
  788.         }
  789.         if (isset($this->_touchedBlocks[$block])) {
  790.             unset($this->_touchedBlocks[$block]);
  791.         }
  792.         $this->_hiddenBlocks[$block] = true;
  793.         return SIGMA_OK;
  794.     }
  795.  
  796.  
  797.    /**
  798.     * Sets the template.
  799.     *
  800.     * You can either load a template file from disk with LoadTemplatefile() or set the
  801.     * template manually using this function.
  802.     * 
  803.     * @access public
  804.     * @param  string      template content
  805.     * @param  boolean     remove unknown/unused variables?
  806.     * @param  boolean     remove empty blocks?
  807.     * @return mixed       SIGMA_OK on success, error object on failure
  808.     * @see    loadTemplatefile()
  809.     */
  810.     function setTemplate($template, $removeUnknownVariables = true, $removeEmptyBlocks = true)
  811.     {
  812.         $this->_resetTemplate($removeUnknownVariables, $removeEmptyBlocks);
  813.         $list = $this->_buildBlocks('<!-- BEGIN __global__ -->'.$template.'<!-- END __global__ -->');
  814.         if (PEAR::isError($list)) {
  815.             return $list;
  816.         }
  817.         $this->_buildBlockVariables();
  818.         return SIGMA_OK;
  819.     }
  820.  
  821.  
  822.    /**
  823.     * Loads a template file.
  824.     * 
  825.     * If caching is on, then it checks whether a "prepared" template exists.
  826.     * If it does, it gets loaded instead of the original, if it does not, then
  827.     * the original gets loaded and prepared and then the prepared version is saved.
  828.     * addBlockfile() and replaceBlockfile() implement quite the same logic.
  829.     *
  830.     * @param    string      filename
  831.     * @param    boolean     remove unknown/unused variables?
  832.     * @param    boolean     remove empty blocks?
  833.     * @access   public
  834.     * @return   mixed       SIGMA_OK on success, error object on failure
  835.     * @see      setTemplate(), $removeUnknownVariables, $removeEmptyBlocks
  836.     */
  837.     function loadTemplateFile($filename, $removeUnknownVariables = true, $removeEmptyBlocks = true)
  838.     {
  839.         if ($this->_isCached($filename)) {
  840.             $this->_resetTemplate($removeUnknownVariables, $removeEmptyBlocks);
  841.             return $this->_getCached($filename);
  842.         }
  843.         $template = $this->_getFile($this->_sourceName($filename));
  844.         if (PEAR::isError($template)) {
  845.             return $template;
  846.         }
  847.         $this->_triggers = array();
  848.         $template = preg_replace($this->includeRegExp, "\$this->_makeTrigger('\\1', '__global__')", $template);
  849.         if (SIGMA_OK !== ($res = $this->setTemplate($template, $removeUnknownVariables, $removeEmptyBlocks))) {
  850.             return $res;
  851.         } else {
  852.             return $this->_writeCache($filename, '__global__');
  853.         }
  854.     }
  855.  
  856.  
  857.    /**
  858.     * Adds a block to the template changing a variable placeholder to a block placeholder.
  859.     *
  860.     * This means that a new block will be integrated into the template in
  861.     * place of a variable placeholder. The variable placeholder will be 
  862.     * removed and the new block will behave in the same way as if it was 
  863.     * inside the original template.
  864.     *
  865.     * The block content must not start with <!-- BEGIN blockname --> and end with
  866.     * <!-- END blockname -->, if it does the error will be thrown.
  867.     * 
  868.     * @param    string    name of the variable placeholder, the name must be unique within the template.
  869.     * @param    string    name of the block to be added
  870.     * @param    string    content of the block
  871.     * @return   mixed     SIGMA_OK on success, error object on failure
  872.     * @throws   PEAR_Error
  873.     * @see      addBlockfile()
  874.     * @access   public
  875.     */
  876.     function addBlock($placeholder, $block, $template)
  877.     {
  878.         if (isset($this->_blocks[$block])) {
  879.             return $this->raiseError($this->errorMessage(SIGMA_BLOCK_EXISTS, $block), SIGMA_BLOCK_EXISTS);
  880.         }
  881.         $parents = $this->_findParentBlocks($placeholder);
  882.         if (0 == count($parents)) {
  883.             return $this->raiseError($this->errorMessage(SIGMA_PLACEHOLDER_NOT_FOUND, $placeholder), SIGMA_PLACEHOLDER_NOT_FOUND);
  884.         } elseif (count($parents) > 1) {
  885.             return $this->raiseError($this->errorMessage(SIGMA_PLACEHOLDER_DUPLICATE, $placeholder), SIGMA_PLACEHOLDER_DUPLICATE);
  886.         }
  887.         
  888.         $template = "<!-- BEGIN $block -->" . $template . "<!-- END $block -->";
  889.         $list     = $this->_buildBlocks($template);
  890.         if (PEAR::isError($list)) {
  891.             return $list;
  892.         }
  893.         $this->_replacePlaceholder($parents[0], $placeholder, $block);
  894.         $this->_buildBlockVariables($block);
  895.         return SIGMA_OK;
  896.     }
  897.     
  898.  
  899.    /**
  900.     * Adds a block taken from a file to the template, changing a variable placeholder 
  901.     * to a block placeholder.
  902.     * 
  903.     * @param      string    name of the variable placeholder
  904.     * @param      string    name of the block to be added
  905.     * @param      string    template file that contains the block
  906.     * @return     mixed     SIGMA_OK on success, error object on failure
  907.     * @throws     PEAR_Error
  908.     * @see        addBlock()
  909.     * @access     public
  910.     */
  911.     function addBlockfile($placeholder, $block, $filename)
  912.     {
  913.         if ($this->_isCached($filename)) {
  914.             return $this->_getCached($filename, $block, $placeholder);
  915.         }
  916.         $template = $this->_getFile($this->_sourceName($filename));
  917.         if (PEAR::isError($template)) {
  918.             return $template;
  919.         }
  920.         $template = preg_replace($this->includeRegExp, "\$this->_makeTrigger('\\1', '{$block}')", $template);
  921.         if (SIGMA_OK !== ($res = $this->addBlock($placeholder, $block, $template))) {
  922.             return $res;
  923.         } else {
  924.             return $this->_writeCache($filename, $block);
  925.         }
  926.     }
  927.  
  928.  
  929.    /**
  930.     * Replaces an existing block with new content.
  931.     * 
  932.     * This function will replace a block of the template and all blocks 
  933.     * contained in it and add a new block instead. This means you can 
  934.     * dynamically change your template.
  935.     * 
  936.     * Sigma analyses the way you've nested blocks and knows which block 
  937.     * belongs into another block. This nesting information helps to make the 
  938.     * API short and simple. Replacing blocks does not only mean that Sigma 
  939.     * has to update the nesting information (relatively time consuming task) 
  940.     * but you have to make sure that you do not get confused due to the 
  941.     * template change yourself.
  942.     * 
  943.     * @param   string    name of a block to replace
  944.     * @param   string    new content
  945.     * @param   boolean   true if the parsed contents of the block should be kept
  946.     * @access  public
  947.     * @see     replaceBlockfile(), addBlock()
  948.     * @return  mixed     SIGMA_OK on success, error object on failure
  949.     * @throws  PEAR_Error
  950.     */
  951.     function replaceBlock($block, $template, $keepContent = false)
  952.     {
  953.         if (!isset($this->_blocks[$block])) {
  954.             return $this->raiseError($this->errorMessage(SIGMA_BLOCK_NOT_FOUND, $block), SIGMA_BLOCK_NOT_FOUND);
  955.         }
  956.         // should not throw a error as we already checked for block existance
  957.         $this->_removeBlockData($block, $keepContent);
  958.         $template = "<!-- BEGIN $block -->" . $template . "<!-- END $block -->";
  959.  
  960.         $list = $this->_buildBlocks($template);
  961.         if (PEAR::isError($list)) {
  962.             return $list;
  963.         }
  964.         // renew the variables list
  965.         $this->_buildBlockVariables($block);
  966.         return SIGMA_OK;
  967.     }
  968.  
  969.  
  970.    /**
  971.     * Replaces an existing block with new content from a file.
  972.     * 
  973.     * @access     public
  974.     * @param      string    name of a block to replace
  975.     * @param      string    template file that contains the block
  976.     * @param      boolean   true if the parsed contents of the block should be kept
  977.     * @return     mixed     SIGMA_OK on success, error object on failure
  978.     * @throws     PEAR_Error
  979.     * @see        replaceBlock(), addBlockfile()
  980.     */
  981.     function replaceBlockfile($block, $filename, $keepContent = false)
  982.     {
  983.         if ($this->_isCached($filename)) {
  984.             if (PEAR::isError($res = $this->_removeBlockData($block, $keepContent))) {
  985.                 return $res;
  986.             } else {
  987.                 return $this->_getCached($filename, $block);
  988.             }
  989.         }
  990.         $template = $this->_getFile($this->_sourceName($filename));
  991.         if (PEAR::isError($template)) {
  992.             return $template;
  993.         }
  994.         $template = preg_replace($this->includeRegExp, "\$this->_makeTrigger('\\1', '{$block}')", $template);
  995.         if (SIGMA_OK !== ($res = $this->replaceBlock($block, $template, $keepContent))) {
  996.             return $res;
  997.         } else {
  998.             return $this->_writeCache($filename, $block);
  999.         }
  1000.     }
  1001.  
  1002.  
  1003.    /**
  1004.     * Checks if the block exists in the template
  1005.     *
  1006.     * @param  string  block name
  1007.     * @return bool
  1008.     * @access public
  1009.     */
  1010.     function blockExists($block)
  1011.     {
  1012.         return isset($this->_blocks[$block]);
  1013.     }
  1014.  
  1015.  
  1016.    /**
  1017.     * Returns the name of the (first) block that contains the specified placeholder.
  1018.     *
  1019.     * @param    string  Name of the placeholder you're searching
  1020.     * @param    string  Name of the block to scan. If left out (default) all blocks are scanned.
  1021.     * @return   string  Name of the (first) block that contains the specified placeholder.
  1022.     *                   If the placeholder was not found an empty string is returned.
  1023.     * @access   public
  1024.     * @throws   PEAR_Error
  1025.     */
  1026.     function placeholderExists($placeholder, $block = '')
  1027.     {
  1028.         if ('' != $block && !isset($this->_blocks[$block])) {
  1029.             return $this->raiseError($this->errorMessage(SIGMA_BLOCK_NOT_FOUND, $block), SIGMA_BLOCK_NOT_FOUND);
  1030.         }
  1031.         if ('' != $block) {
  1032.             // if we search in the specific block, we should just check the array
  1033.             return isset($this->_blockVariables[$block][$placeholder])? $block: '';
  1034.         } else {
  1035.             // _findParentBlocks returns an array, we need only the first element
  1036.             $parents = $this->_findParentBlocks($placeholder);
  1037.             return empty($parents)? '': $parents[0];
  1038.         }
  1039.     } // end func placeholderExists
  1040.  
  1041.  
  1042.    /**
  1043.     * Sets a callback function.
  1044.     *
  1045.     * Sigma templates can contain simple function calls. This means that the 
  1046.     * author of the template can add a special placeholder to it: 
  1047.     * func_h1("embedded in h1")
  1048.     * Sigma will parse the template for these placeholders and will allow 
  1049.     * you to define a callback function for them. Callback will be called 
  1050.     * automatically when the block containing such function call is parse()'d.
  1051.     *
  1052.     * Please note that arguments to these template functions can contain 
  1053.     * variable placeholders: func_translate('Hello, {username}'), but not 
  1054.     * blocks or other function calls.
  1055.     * 
  1056.     * This should NOT be used to add logic (except some presentation one) to
  1057.     * the template. If you use a lot of such callbacks and implement business
  1058.     * logic through them, then you're reinventing the wheel. Consider using
  1059.     * XML/XSLT, native PHP or some other template engine.
  1060.     *
  1061.     * <?php
  1062.     * function h_one($arg) {
  1063.     *    return '<h1>' . $arg . '</h1>';
  1064.     * }
  1065.     * ...
  1066.     * $tpl = new HTML_Template_Sigma( ... );
  1067.     * ...
  1068.     * $tpl->setCallbackFunction('h1', 'h_one');
  1069.     * ?>
  1070.     *
  1071.     * template:
  1072.     * func_h1('H1 Headline');
  1073.     *
  1074.     * @param    string    Function name in the template
  1075.     * @param    mixed     A callback: anything that can be passed to call_user_func_array()
  1076.     * @param    bool      If true, then no variable substitution in arguments will take place before function call
  1077.     * @return   mixed     SIGMA_OK on success, error object on failure
  1078.     * @throws   PEAR_Error
  1079.     * @access   public
  1080.     */
  1081.     function setCallbackFunction($tplFunction, $callback, $preserveArgs = false)
  1082.     {
  1083.         if (!is_callable($callback)) {
  1084.             return $this->raiseError($this->errorMessage(SIGMA_INVALID_CALLBACK), SIGMA_INVALID_CALLBACK);
  1085.         }
  1086.         $this->_callback[$tplFunction] = array(
  1087.             'data'         => $callback,
  1088.             'preserveArgs' => $preserveArgs
  1089.         );
  1090.         return SIGMA_OK;
  1091.     } // end func setCallbackFunction
  1092.  
  1093.  
  1094.    /**
  1095.     * Returns a list of blocks within a template.
  1096.     *
  1097.     * If $recursive is false, it returns just a 'flat' array of $parent's
  1098.     * direct subblocks. If $recursive is true, it builds a tree of template
  1099.     * blocks using $parent as root. Tree structure is compatible with 
  1100.     * PEAR::Tree's Memory_Array driver.
  1101.     * 
  1102.     * @param    string  parent block name 
  1103.     * @param    bool    whether to return a tree of child blocks (true) or a 'flat' array (false)
  1104.     * @access   public
  1105.     * @return   array   a list of child blocks
  1106.     * @throws   PEAR_Error
  1107.     */
  1108.     function getBlockList($parent = '__global__', $recursive = false)
  1109.     {
  1110.         if (!isset($this->_blocks[$parent])) {
  1111.             return $this->raiseError($this->errorMessage(SIGMA_BLOCK_NOT_FOUND, $parent), SIGMA_BLOCK_NOT_FOUND);
  1112.         }
  1113.         if (!$recursive) {
  1114.             return isset($this->_children[$parent])? array_keys($this->_children[$parent]): array();
  1115.         } else {
  1116.             $ret = array('name' => $parent);
  1117.             if (!empty($this->_children[$parent])) {
  1118.                 $ret['children'] = array();
  1119.                 foreach (array_keys($this->_children[$parent]) as $child) {
  1120.                     $ret['children'][] = $this->getBlockList($child, true);
  1121.                 }
  1122.             }
  1123.             return $ret;
  1124.         }
  1125.     }
  1126.  
  1127.  
  1128.    /**
  1129.     * Returns a list of placeholders within a block.
  1130.     * 
  1131.     * Only 'normal' placeholders are returned, not auto-created ones.
  1132.     *
  1133.     * @param    string  block name
  1134.     * @access   public
  1135.     * @return   array   a list of placeholders
  1136.     * @throws   PEAR_Error
  1137.     */
  1138.     function getPlaceholderList($block = '__global__')
  1139.     {
  1140.         if (!isset($this->_blocks[$block])) {
  1141.             return $this->raiseError($this->errorMessage(SIGMA_BLOCK_NOT_FOUND, $block), SIGMA_BLOCK_NOT_FOUND);
  1142.         }
  1143.         $ret = array();
  1144.         foreach ($this->_blockVariables[$block] as $var => $v) {
  1145.             if ('__' != substr($var, 0, 2) || '__' != substr($var, -2)) {
  1146.                 $ret[] = $var;
  1147.             }
  1148.         }
  1149.         return $ret;
  1150.     }
  1151.  
  1152.  
  1153.    /**
  1154.     * Clears the variables
  1155.     * 
  1156.     * Global variables are not affected. The method is useful when you add
  1157.     * a lot of variables via setVariable() and are not sure whether all of 
  1158.     * them appear in the block you parse(). If you clear the variables after
  1159.     * parse(), you don't risk them suddenly showing up in other blocks.
  1160.     * 
  1161.     * @access public
  1162.     * @see    setVariable()
  1163.     */
  1164.     function clearVariables()
  1165.     {
  1166.         $this->_variables = array();
  1167.     }
  1168.  
  1169.  
  1170.     //------------------------------------------------------------
  1171.     //
  1172.     // Private methods follow
  1173.     //
  1174.     //------------------------------------------------------------
  1175.  
  1176.  
  1177.    /**
  1178.     * Reads the file and returns its content
  1179.     * 
  1180.     * @param    string    filename
  1181.     * @return   string    file content (or error object)
  1182.     * @access   private
  1183.     */    
  1184.     function _getFile($filename)
  1185.     {
  1186.         if (!($fh = @fopen($filename, 'r'))) {
  1187.             return $this->raiseError($this->errorMessage(SIGMA_TPL_NOT_FOUND, $filename), SIGMA_TPL_NOT_FOUND);
  1188.         }
  1189.         $content = fread($fh, filesize($filename));
  1190.         fclose($fh);
  1191.         return $content;
  1192.     }
  1193.  
  1194.  
  1195.    /**
  1196.     * Recursively builds a list of all variables within a block.
  1197.     *
  1198.     * Also calls _buildFunctionlist() for each block it visits
  1199.     * 
  1200.     * @param    string block name
  1201.     * @see      _buildFunctionlist()
  1202.     * @access   private
  1203.     */
  1204.     function _buildBlockVariables($block = '__global__')
  1205.     {
  1206.         $this->_blockVariables[$block] = array();
  1207.         $this->_functions[$block]      = array();
  1208.         preg_match_all($this->variablesRegExp, $this->_blocks[$block], $regs, PREG_SET_ORDER);
  1209.         foreach ($regs as $match) {
  1210.             $this->_blockVariables[$block][$match[1]] = true;
  1211.             if (!empty($match[3])) {
  1212.                 $funcData = array(
  1213.                     'name' => $match[3],
  1214.                     'args' => array($this->openingDelimiter . $match[1] . $this->closingDelimiter)
  1215.                 );
  1216.                 $funcId   = substr(md5(serialize($funcData)), 0, 10);
  1217.  
  1218.                 // update block info
  1219.                 $this->_blocks[$block] = str_replace($match[0], '{__function_' . $funcId . '__}', $this->_blocks[$block]);
  1220.                 $this->_blockVariables[$block]['__function_' . $funcId . '__'] = true;
  1221.                 $this->_functions[$block][$funcId] = $funcData;
  1222.             }
  1223.         }
  1224.         $this->_buildFunctionlist($block);
  1225.         if (isset($this->_children[$block]) && is_array($this->_children[$block])) {
  1226.             foreach ($this->_children[$block] as $child => $v) {
  1227.                 $this->_buildBlockVariables($child);
  1228.             }
  1229.         }
  1230.     }
  1231.  
  1232.  
  1233.    /**
  1234.     * Recusively builds a list of all blocks within the template.
  1235.     * 
  1236.     * @param    string    template to be scanned
  1237.     * @see      $_blocks
  1238.     * @throws   PEAR_Error
  1239.     * @return   mixed     array of block names on success or error object on failure
  1240.     * @access   private
  1241.     */
  1242.     function _buildBlocks($string)
  1243.     {
  1244.         $blocks = array();
  1245.         if (preg_match_all($this->blockRegExp, $string, $regs, PREG_SET_ORDER)) {
  1246.             foreach ($regs as $k => $match) {
  1247.                 $blockname    = $match[1];
  1248.                 $blockcontent = $match[2];
  1249.                 if (isset($this->_blocks[$blockname]) || isset($blocks[$blockname])) {
  1250.                     return $this->raiseError($this->errorMessage(SIGMA_BLOCK_DUPLICATE, $blockname), SIGMA_BLOCK_DUPLICATE);
  1251.                 }
  1252.                 $this->_blocks[$blockname] = $blockcontent;
  1253.                 $blocks[$blockname] = true;
  1254.                 $inner              = $this->_buildBlocks($blockcontent);
  1255.                 if (PEAR::isError($inner)) {
  1256.                     return $inner;
  1257.                 }
  1258.                 foreach ($inner as $name => $v) {
  1259.                     $pattern     = sprintf('@<!--\s+BEGIN\s+%s\s+-->(.*)<!--\s+END\s+%s\s+-->@sm', $name, $name);
  1260.                     $replacement = $this->openingDelimiter.'__'.$name.'__'.$this->closingDelimiter;
  1261.                     $this->_blocks[$blockname]          = preg_replace($pattern, $replacement, $this->_blocks[$blockname]);
  1262.                     $this->_children[$blockname][$name] = true;
  1263.                 }
  1264.             }
  1265.         }
  1266.         return $blocks;
  1267.     }
  1268.  
  1269.  
  1270.    /**
  1271.     * Resets the object's properties, used before processing a new template
  1272.     *
  1273.     * @access   private
  1274.     * @param    boolean     remove unknown/unused variables?
  1275.     * @param    boolean     remove empty blocks?
  1276.     * @see      setTemplate(), loadTemplateFile()
  1277.     * @access   private
  1278.     */
  1279.     function _resetTemplate($removeUnknownVariables = true, $removeEmptyBlocks = true)
  1280.     {
  1281.         $this->removeUnknownVariables = $removeUnknownVariables;
  1282.         $this->removeEmptyBlocks      = $removeEmptyBlocks;
  1283.         $this->currentBlock           = '__global__';
  1284.         $this->_variables             = array();
  1285.         $this->_blocks                = array();
  1286.         $this->_children              = array();
  1287.         $this->_parsedBlocks          = array();
  1288.         $this->_touchedBlocks         = array();
  1289.         $this->_functions             = array();
  1290.         $this->flagGlobalParsed       = false;
  1291.     } // _resetTemplate
  1292.  
  1293.  
  1294.    /**
  1295.     * Checks whether we have a "prepared" template cached.
  1296.     * 
  1297.     * If we do not do caching, always returns false
  1298.     * 
  1299.     * @access private
  1300.     * @param  string source filename
  1301.     * @return bool yes/no
  1302.     * @see loadTemplatefile(), addBlockfile(), replaceBlockfile()
  1303.     */
  1304.     function _isCached($filename)
  1305.     {
  1306.         if (null === $this->_cacheRoot) {
  1307.             return false;
  1308.         }
  1309.         $cachedName = $this->_cachedName($filename);
  1310.         $sourceName = $this->_sourceName($filename);
  1311.         // if $sourceName does not exist, error will be thrown later
  1312.         $sourceTime = @filemtime($sourceName);
  1313.         if ((false !== $sourceTime) && @file_exists($cachedName) && (filemtime($cachedName) > $sourceTime)) {
  1314.             return true;
  1315.         } else {
  1316.             return false;
  1317.         }
  1318.     } // _isCached
  1319.  
  1320.  
  1321.    /**
  1322.     * Loads a "prepared" template file
  1323.     *
  1324.     * @access   private
  1325.     * @param    string  filename
  1326.     * @param    string  block name
  1327.     * @param    string  variable placeholder to replace by a block
  1328.     * @return   mixed   SIGMA_OK on success, error object on failure
  1329.     * @see loadTemplatefile(), addBlockfile(), replaceBlockfile()
  1330.     */
  1331.     function _getCached($filename, $block = '__global__', $placeholder = '')
  1332.     {
  1333.         // the same checks are done in addBlock()
  1334.         if (!empty($placeholder)) {
  1335.             if (isset($this->_blocks[$block])) {
  1336.                 return $this->raiseError($this->errorMessage(SIGMA_BLOCK_EXISTS, $block), SIGMA_BLOCK_EXISTS);
  1337.             }
  1338.             $parents = $this->_findParentBlocks($placeholder);
  1339.             if (0 == count($parents)) {
  1340.                 return $this->raiseError($this->errorMessage(SIGMA_PLACEHOLDER_NOT_FOUND, $placeholder), SIGMA_PLACEHOLDER_NOT_FOUND);
  1341.             } elseif (count($parents) > 1) {
  1342.                 return $this->raiseError($this->errorMessage(SIGMA_PLACEHOLDER_DUPLICATE, $placeholder), SIGMA_PLACEHOLDER_DUPLICATE);
  1343.             }
  1344.         }
  1345.         $content = $this->_getFile($this->_cachedName($filename));
  1346.         if (PEAR::isError($content)) {
  1347.             return $content;
  1348.         }
  1349.         $cache = unserialize($content);
  1350.         if ('__global__' != $block) {
  1351.             $this->_blocks[$block]         = $cache['blocks']['__global__'];
  1352.             $this->_blockVariables[$block] = $cache['variables']['__global__'];
  1353.             $this->_children[$block]       = $cache['children']['__global__'];
  1354.             $this->_functions[$block]      = $cache['functions']['__global__'];
  1355.             unset($cache['blocks']['__global__'], $cache['variables']['__global__'], $cache['children']['__global__'], $cache['functions']['__global__']);
  1356.         }
  1357.         $this->_blocks         = array_merge($this->_blocks, $cache['blocks']);
  1358.         $this->_blockVariables = array_merge($this->_blockVariables, $cache['variables']);
  1359.         $this->_children       = array_merge($this->_children, $cache['children']);
  1360.         $this->_functions      = array_merge($this->_functions, $cache['functions']);
  1361.  
  1362.         // the same thing gets done in addBlockfile()
  1363.         if (!empty($placeholder)) {
  1364.             $this->_replacePlaceholder($parents[0], $placeholder, $block);
  1365.         }
  1366.         // pull the triggers, if any
  1367.         if (isset($cache['triggers'])) {
  1368.             return $this->_pullTriggers($cache['triggers']);
  1369.         }
  1370.         return SIGMA_OK;
  1371.     } // _getCached
  1372.  
  1373.  
  1374.    /**
  1375.     * Returns a full name of a "prepared" template file
  1376.     * 
  1377.     * @access private
  1378.     * @param string  source filename, relative to root directory
  1379.     * @return string filename
  1380.     */
  1381.     function _cachedName($filename)
  1382.     {
  1383.         if ('/' == $filename{0} && '/' == substr($this->_cacheRoot, -1)) {
  1384.             $filename = substr($filename, 1);
  1385.         }
  1386.         $filename = str_replace('/', '__', $filename);
  1387.         return $this->_cacheRoot. $filename. '.it';
  1388.     } // _cachedName
  1389.  
  1390.  
  1391.    /**
  1392.     * Returns a full name of a "source" template file
  1393.     *
  1394.     * @param string   source filename, relative to root directory
  1395.     * @access private
  1396.     * @return string
  1397.     */
  1398.     function _sourceName($filename)
  1399.     {
  1400.         if ('/' == $filename{0} && '/' == substr($this->fileRoot, -1)) {
  1401.             $filename = substr($filename, 1);
  1402.         }
  1403.         return $this->fileRoot . $filename;
  1404.     } // _sourceName
  1405.  
  1406.  
  1407.    /**
  1408.     * Writes a prepared template file.
  1409.     * 
  1410.     * Even if NO caching is going on, this method has a side effect: it calls 
  1411.     * the _pullTriggers() method and thus loads all files added via <!-- INCLUDE -->
  1412.     *
  1413.     * @access private
  1414.     * @param string   source filename, relative to root directory
  1415.     * @param string   name of the block to save into file
  1416.     * @return mixed   SIGMA_OK on success, error object on failure
  1417.     */
  1418.     function _writeCache($filename, $block)
  1419.     {
  1420.         // do not save anything if no cache dir, but do pull triggers
  1421.         if (null !== $this->_cacheRoot) {
  1422.             $cache = array(
  1423.                 'blocks'    => array(),
  1424.                 'variables' => array(),
  1425.                 'children'  => array(),
  1426.                 'functions' => array()
  1427.             );
  1428.             $cachedName = $this->_cachedName($filename);
  1429.             $this->_buildCache($cache, $block);
  1430.             if ('__global__' != $block) {
  1431.                 foreach (array_keys($cache) as $k) {
  1432.                     $cache[$k]['__global__'] = $cache[$k][$block];
  1433.                     unset($cache[$k][$block]);
  1434.                 }
  1435.             }
  1436.             if (isset($this->_triggers[$block])) {
  1437.                 $cache['triggers'] = $this->_triggers[$block];
  1438.             }
  1439.             if (!($fh = @fopen($cachedName, 'w'))) {
  1440.                 return $this->raiseError($this->errorMessage(SIGMA_CACHE_ERROR, $cachedName), SIGMA_CACHE_ERROR);
  1441.             }
  1442.             fwrite($fh, serialize($cache));
  1443.             fclose($fh);
  1444.         }
  1445.         // now pull triggers
  1446.         if (isset($this->_triggers[$block])) {
  1447.             if (SIGMA_OK !== ($res = $this->_pullTriggers($this->_triggers[$block]))) {
  1448.                 return $res;
  1449.             }
  1450.             unset($this->_triggers[$block]);
  1451.         }
  1452.         return SIGMA_OK;
  1453.     } // _writeCache
  1454.  
  1455.  
  1456.    /**
  1457.     * Builds an array of template data to be saved in prepared template file
  1458.     *
  1459.     * @access private
  1460.     * @param array   template data
  1461.     * @param string  block to add to the array
  1462.     */
  1463.     function _buildCache(&$cache, $block)
  1464.     {
  1465.         if (!$this->_options['trim_on_save']) {
  1466.             $cache['blocks'][$block] = $this->_blocks[$block];
  1467.         } else {
  1468.             $cache['blocks'][$block] = preg_replace(
  1469.                                          array('/^\\s+/m', '/\\s+$/m', '/(\\r?\\n)+/'),
  1470.                                          array('', '', "\n"),
  1471.                                          $this->_blocks[$block]
  1472.                                        );
  1473.         }
  1474.         $cache['variables'][$block] = $this->_blockVariables[$block];
  1475.         $cache['functions'][$block] = isset($this->_functions[$block])? $this->_functions[$block]: array();
  1476.         if (!isset($this->_children[$block])) {
  1477.             $cache['children'][$block] = array();
  1478.         } else {
  1479.             $cache['children'][$block] = $this->_children[$block];
  1480.             foreach (array_keys($this->_children[$block]) as $child) {
  1481.                 $this->_buildCache($cache, $child);
  1482.             }
  1483.         }
  1484.     }
  1485.  
  1486.  
  1487.    /**
  1488.     * Recursively removes all data belonging to a block
  1489.     * 
  1490.     * @param    string    block name
  1491.     * @param    boolean   true if the parsed contents of the block should be kept
  1492.     * @return   mixed     SIGMA_OK on success, error object on failure
  1493.     * @see      replaceBlock(), replaceBlockfile()
  1494.     * @access   private
  1495.     */
  1496.     function _removeBlockData($block, $keepContent = false)
  1497.     {
  1498.         if (!isset($this->_blocks[$block])) {
  1499.             return $this->raiseError($this->errorMessage(SIGMA_BLOCK_NOT_FOUND, $block), SIGMA_BLOCK_NOT_FOUND);
  1500.         }
  1501.         if (!empty($this->_children[$block])) {
  1502.             foreach (array_keys($this->_children[$block]) as $child) {
  1503.                 $this->_removeBlockData($child, false);
  1504.             }
  1505.             unset($this->_children[$block]);
  1506.         }
  1507.         unset($this->_blocks[$block]);
  1508.         unset($this->_blockVariables[$block]);
  1509.         unset($this->_hiddenBlocks[$block]);
  1510.         unset($this->_touchedBlocks[$block]);
  1511.         unset($this->_functions[$block]);
  1512.         if (!$keepContent) {
  1513.             unset($this->_parsedBlocks[$block]);
  1514.         }
  1515.         return SIGMA_OK;
  1516.     }
  1517.  
  1518.  
  1519.    /**
  1520.     * Returns the names of the blocks where the variable placeholder appears
  1521.     *
  1522.     * @param    string    variable name
  1523.     * @return    array    block names
  1524.     * @see addBlock(), addBlockfile(), placeholderExists()
  1525.     * @access   private
  1526.     */
  1527.     function _findParentBlocks($variable)
  1528.     {
  1529.         $parents = array();
  1530.         foreach ($this->_blockVariables as $blockname => $varnames) {
  1531.             if (!empty($varnames[$variable])) {
  1532.                 $parents[] = $blockname;
  1533.             }
  1534.         }
  1535.         return $parents;
  1536.     }
  1537.  
  1538.  
  1539.    /**
  1540.     * Replaces a variable placeholder by a block placeholder.
  1541.     * 
  1542.     * Of course, it also updates the necessary arrays
  1543.     * 
  1544.     * @param    string  name of the block containing the placeholder
  1545.     * @param    string  variable name
  1546.     * @param    string  block name
  1547.     * @access   private
  1548.     */
  1549.     function _replacePlaceholder($parent, $placeholder, $block)
  1550.     {
  1551.         $this->_children[$parent][$block] = true;
  1552.         $this->_blockVariables[$parent]['__'.$block.'__'] = true;
  1553.         $this->_blocks[$parent]    = str_replace($this->openingDelimiter.$placeholder.$this->closingDelimiter,
  1554.                                                         $this->openingDelimiter.'__'.$block.'__'.$this->closingDelimiter,
  1555.                                                         $this->_blocks[$parent] );
  1556.         unset($this->_blockVariables[$parent][$placeholder]);
  1557.     }
  1558.  
  1559.  
  1560.    /**
  1561.     * Generates a placeholder to replace an <!-- INCLUDE filename --> statement
  1562.     * 
  1563.     * @access   private
  1564.     * @param    string  filename
  1565.     * @param    string  current block name
  1566.     * @return   string  a placeholder
  1567.     */
  1568.     function _makeTrigger($filename, $block)
  1569.     {
  1570.         $name = 'trigger_' . substr(md5($filename . ' ' . uniqid($block)), 0, 10);
  1571.         $this->_triggers[$block][$name] = $filename;
  1572.         return $this->openingDelimiter . $name . $this->closingDelimiter;
  1573.     }
  1574.  
  1575.  
  1576.    /**
  1577.     * Replaces the "trigger" placeholders by the matching file contents.
  1578.     * 
  1579.     * @see _makeTrigger(), addBlockfile()
  1580.     * @param    array   array ('trigger placeholder' => 'filename')
  1581.     * @return   mixed   SIGMA_OK on success, error object on failure
  1582.     * @access   private
  1583.     */
  1584.     function _pullTriggers($triggers)
  1585.     {
  1586.         foreach ($triggers as $placeholder => $filename) {
  1587.             if (SIGMA_OK !== ($res = $this->addBlockfile($placeholder, $placeholder, $filename))) {
  1588.                 return $res;
  1589.             }
  1590.             // we actually do not need the resultant block...
  1591.             $parents = $this->_findParentBlocks('__' . $placeholder . '__');
  1592.             // merge current block's children and variables with the parent's ones
  1593.             if (isset($this->_children[$placeholder])) {
  1594.                 $this->_children[$parents[0]] = array_merge($this->_children[$parents[0]], $this->_children[$placeholder]);
  1595.             }
  1596.             $this->_blockVariables[$parents[0]] = array_merge($this->_blockVariables[$parents[0]], $this->_blockVariables[$placeholder]);
  1597.             if (isset($this->_functions[$placeholder])) {
  1598.                 $this->_functions[$parents[0]] = array_merge($this->_functions[$parents[0]], $this->_functions[$placeholder]);
  1599.             }
  1600.             // substitute the block's contents into parent's
  1601.             $this->_blocks[$parents[0]] = str_replace(
  1602.                                             $this->openingDelimiter . '__' . $placeholder . '__' . $this->closingDelimiter, 
  1603.                                             $this->_blocks[$placeholder], 
  1604.                                             $this->_blocks[$parents[0]]
  1605.                                           );
  1606.             // remove the stuff that is no more needed
  1607.             unset($this->_blocks[$placeholder], $this->_blockVariables[$placeholder], $this->_children[$placeholder], $this->_functions[$placeholder]);
  1608.             unset($this->_children[$parents[0]][$placeholder], $this->_blockVariables[$parents[0]]['__' . $placeholder . '__']);
  1609.         }
  1610.         return SIGMA_OK;
  1611.     }
  1612.  
  1613.  
  1614.    /**
  1615.     * Builds a list of functions in a block.
  1616.     *
  1617.     * @access   private
  1618.     * @param    string  Block name
  1619.     * @see _buildBlockVariables()
  1620.     */
  1621.     function _buildFunctionlist($block)
  1622.     {
  1623.         $template = $this->_blocks[$block];
  1624.  
  1625.         while (preg_match($this->functionRegExp, $template, $regs)) {
  1626.  
  1627.             $template = substr($template, strpos($template, $regs[0]) + strlen($regs[0]));
  1628.             $head     = $this->_getToken($template, ')');
  1629.             $funcText = $regs[0] . $head . ')';
  1630.             $funcData = array(
  1631.                 'name' => $regs[1],
  1632.                 'args' => array()
  1633.             );
  1634.             // build function arguments
  1635.             while (('' != $head) && ('' != trim($arg = $this->_getToken($head, ',')))) {
  1636.                 $arg2 = trim($arg);
  1637.                 $funcData['args'][] = ('"' == $arg2{0} || "'" == $arg2{0}) ? substr($arg2, 1, -1) : $arg2;
  1638.                 if ($arg == $head) {
  1639.                     break;
  1640.                 }
  1641.                 $head = substr($head, strlen($arg) + 1);
  1642.             }
  1643.             $funcId   = substr(md5(serialize($funcData)), 0, 10);
  1644.             $template = str_replace($funcText, '{__function_' . $funcId . '__}', $template);
  1645.  
  1646.             // update block info
  1647.             $this->_blocks[$block] = str_replace($funcText, '{__function_' . $funcId . '__}', $this->_blocks[$block]);
  1648.             $this->_blockVariables[$block]['__function_' . $funcId . '__'] = true;
  1649.             $this->_functions[$block][$funcId] = $funcData;
  1650.         }
  1651.     } // end func _buildFunctionlist
  1652.  
  1653.  
  1654.    /**
  1655.     * Returns a part of string up to a delimiter.
  1656.     * 
  1657.     * It should handle strings enclosed in single and double quotes, 
  1658.     * thus the need for a non-trivial function
  1659.     * 
  1660.     * @param    string  a string from which to extract
  1661.     * @param    mixed   a delimiter or an array of ('delimiter' => true)
  1662.     * @return   string  an extracted string
  1663.     * @access   private
  1664.     */
  1665.     function _getToken($code, $delimiter)
  1666.     {
  1667.         if (!is_array($delimiter)) {
  1668.             $delimiter = array( $delimiter => true );
  1669.         }
  1670.         if ('' == $code || isset($delimiter[$code{0}])) {
  1671.             return '';
  1672.         }
  1673.  
  1674.         $len         = strlen($code);
  1675.         $enclosed    = false;
  1676.         $enclosed_by = '';
  1677.  
  1678.         for ($i = 0; $i < $len; $i++) {
  1679.             $char = $code{$i};
  1680.  
  1681.             if (('"' == $char || "'" == $char) && 
  1682.                 ($char == $enclosed_by || '' == $enclosed_by) && 
  1683.                 (0 == $i || ($i > 0 && '\\' != $code{$i - 1}))) 
  1684.             {
  1685.                 $enclosed_by = $enclosed? '': $char;
  1686.                 $enclosed    = !$enclosed;
  1687.             }
  1688.             if (!$enclosed && isset($delimiter[$char])) {
  1689.                 break;
  1690.             }
  1691.         }
  1692.         return substr($code, 0, $i);
  1693.     } // end func _getToken
  1694.  
  1695.  
  1696.    /**
  1697.     * Replaces an opening delimiter by a special string.
  1698.     * 
  1699.     * Used to implement $_options['preserve_data'] logic
  1700.     * 
  1701.     * @access   private
  1702.     * @param string
  1703.     * @return string
  1704.     */
  1705.     function _preserveOpeningDelimiter($str)
  1706.     {
  1707.         return (false === strpos($str, $this->openingDelimiter))? 
  1708.                 $str:
  1709.                 str_replace($this->openingDelimiter, $this->openingDelimiter . '%preserved%' . $this->closingDelimiter, $str);
  1710.     }
  1711.  
  1712.  
  1713.    /**
  1714.     * Quotes the string so that it can be used in Javascript string constants
  1715.     *
  1716.     * @access private
  1717.     * @param  string
  1718.     * @return string
  1719.     */
  1720.     function _jsEscape($value)
  1721.     {
  1722.         return strtr($value, array(
  1723.                     "\r" => '\r', "'"  => "\\'", "\n" => '\n', 
  1724.                     '"'  => '\"', "\t" => '\t',  '\\' => '\\\\'
  1725.                ));
  1726.     }
  1727. }
  1728. ?>
  1729.